"""
简单实现循环神经网络---裸写一个退位减法器
输入两个数相减时，一旦发生退位运算，需要将中间的状态保存起来
当高位的数传入时将退位标志一并传入参加运算
"""
import copy
import numpy as np

np.random.seed(0)


# sigmoid函数及其导数
def sigmoid(_x_):
    output = 1 / (1 + np.exp(-_x_))
    return output


def sigmoid_output_to_derivative(output):
    return output * (1 - output)


# 建立二进制映射
int2binary = {}  # 整数到二进制的映射
binary_dim = 8  # 256内的减法
largest_number = pow(2, binary_dim)
binary = np.unpackbits(np.array([range(largest_number)], dtype=np.uint8).T, axis=1)
for i in range(largest_number):
    int2binary[i] = binary[i]

"""
定义学习参数
隐藏层的权重synapse_0、循环节点的权重synapse_h(输入节点16、输出节点16)、
输出层的权重synapse_1(输入节点16、输出节点1)。为了减少复杂度，只设置了w权重、b被忽略
"""
# 参数设置
alpha = 0.9
input_dim = 2
hidden_dim = 16
output_dim = 1

# 初始化网络
synapse_0 = (2 * np.random.random((input_dim, hidden_dim)) - 1) * 0.05  # => [0.05, 0.05)
synapse_1 = (2 * np.random.random((hidden_dim, output_dim)) - 1) * 0.05
synapse_h = (2 * np.random.random((hidden_dim, hidden_dim)) - 1) * 0.05

# 用于存放反向传播的权重更新值
synapse_0_update = np.zeros_like(synapse_0)
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)

"""
准备样本数据
1）建立循环生成样本数据，先生成两个数据a、b。如果a小于b，就交换位置，保证被减数大
2）计算出相减的结果c
3）将3个数转换成二进制数，为模型计算做准备
"""
# 开始训练
for i in range(10000):
    # 生成一个数字 a
    a_int = np.random.randint(largest_number)
    # 生成一个数字b，b的最大值取的是 largest_number / 2,作为被减数，让它小一点
    b_int = np.random.randint(largest_number / 2)
    # 如果b大了，那么交换一下
    if a_int < b_int:
        a_int, b_int = b_int, a_int

    a = int2binary[a_int]
    b = int2binary[b_int]
    # 正确的答案
    c_int = a_int - b_int
    c = int2binary[c_int]

    """
    模型初始化
    初始化输出值为0，初始化总误差为0，定义layer_2_deltas存储反向传播过程中的循环层误差
    layer_1_values 为隐藏层的输出值，由于第一个数传入时，没有前面的隐藏层输出值作为本次的输入
    所以需要为其定义一个初始值，这里定义为 0.1
    """
    # 存储神经网络的预测值
    d = np.zeros_like(c)
    overallError = 0  # 每次把总误差清零

    layer_2_deltas = list()  # 存储每个时间点输出层的误差
    layer_1_values = list()  # 存储每个时间点隐藏层的值

    layer_1_values.append(np.ones(hidden_dim) * 0.1)  # 一开始没有隐藏层，所以初始化一下原始值为0.1

    """
    正向传播
    循环遍历每个二进制位，从个位开始依次相减，并将中间隐藏的输出传入下一位的计算(退位减法)
    把每一个时间点的误差导数记录下来，同时统计总误差，为输出准备
    """
    for position in range(binary_dim):
        # 生成输入和输出
        # 从左到右，每次取两个数字的一个bit位
        X = np.array([[a[binary_dim - position - 1], b[binary_dim - position - 1]]])
        y = np.array([[b[binary_dim - position - 1]]])  # 正确答案

        # hidden layer (input ~+ prev_hidden)
        # (输入层 + 之前的隐藏层) -> 新的隐藏层，这是体现循环神经网络的核心的地方
        layer_1 = sigmoid(np.dot(X, synapse_0) + np.dot(layer_1_values[-1], synapse_h))

        # output layer (new binary representation)
        # 隐藏层 * 隐藏层到输出层的转化矩阵 synapse_1 -> 输出层
        layer_2 = sigmoid(np.dot(layer_1, synapse_1))

        layer_2_error = y - layer_2  # 预测误差
        # 把每一个时间点的误差导数都记录下来
        layer_2_deltas.append(layer_2_error * sigmoid_output_to_derivative(layer_2))

        overallError += np.abs(layer_2_error[0])  # 总误差

        # 记录每一个预测bit位
        d[binary_dim - position - 1] = np.round(layer_2[0][0])

        # 将隐藏层保存起来，下个时间序列便可以使用
        layer_1_values.append((copy.deepcopy(layer_1)))  # 记录下隐藏层的值，在下一个时间点用

        '''
        为了反向传播做的初始化。同正向传播一样，反向传播是从最后一次往前反向计算误差
        对于每一个当前的计算都需要有它的下一次结果参与
        反向计算是从最后一次开始的，它没有后一次的输出，所以也要初始化一个值，这里为 0
        '''
        future_layer_1_deltas = np.zeros(hidden_dim)

    """
    反向传播
    初始化之后，开始从高位往回遍历，一次对每一位的所有层计算误差，并根据每层误差对权重求偏导
    得到其调整值，最终将每一位算出的各层权重的调整值加在一起乘以学习率，来更新各层的权重，
    完成一次优化训练
    """
    # 反向传播，从最后一各时间点到第一个时间点
    for position in range(binary_dim):
        X = np.array([[a[position], b[position]]])  # 最后一次的两个输入
        layer_1 = layer_1_values[-position - 1]  # 当前时间点的隐藏层
        prev_layer_1 = layer_1_values[-position - 2]  # 前一个时间点的隐藏层
        layer_2_delta = layer_2_deltas[-position - 1]  # 当前时间点输出层的导数
        # 通过后一个时间点(因为是反向传播)的隐藏层误差和当前时间点的输出层误差，计算当前时间点的隐藏层误差
        layer_1_delta = (future_layer_1_deltas.dot(synapse_h.T)
                         + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)
        # 等完成了所有反向传播误差计算，才会更新权重矩阵，先暂时把更新矩阵存储起来
        synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
        synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
        synapse_0_update += X.T.dot(layer_1_delta)

        future_layer_1_deltas = layer_1_delta

        # 完成所有反向传播之后，更新权重值，并把矩阵变量清零
        synapse_0 += synapse_0_update * alpha
        synapse_1 += synapse_1_update * alpha
        synapse_h += synapse_h_update * alpha

        synapse_0_update *= 0
        synapse_1_update *= 0
        synapse_h_update *= 0

    # 输出结果
    if i % 800 == 0:
        print('总误差： ', str(overallError))
        print('Pred： ', str(d))
        print('True： ', str(c))

        out = 0
        for index, x in enumerate(reversed(d)):
            out += x * pow(2, index)
        print(str(a_int) + '-' + str(b_int) + '=' + str(out))
        print('-----------------------------')
